package com.xxl.job.admin.service.impl;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import javax.annotation.Resource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import com.xxl.job.admin.core.cron.CronExpression;
import com.xxl.job.admin.core.model.XxlJobGroup;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobLogReport;
import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum;
import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum;
import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum;
import com.xxl.job.admin.core.thread.JobScheduleHelper;
import com.xxl.job.admin.core.thread.JobTriggerPoolHelper;
import com.xxl.job.admin.core.trigger.TriggerTypeEnum;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.dao.XxlJobGroupDao;
import com.xxl.job.admin.dao.XxlJobInfoDao;
import com.xxl.job.admin.dao.XxlJobLogDao;
import com.xxl.job.admin.dao.XxlJobLogGlueDao;
import com.xxl.job.admin.dao.XxlJobLogReportDao;
import com.xxl.job.admin.service.XxlJobService;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
import com.xxl.job.core.glue.GlueTypeEnum;
import com.xxl.job.core.util.DateUtil;

/**
 * core job action for xxl-job
 * 
 * @author xuxueli 2016-5-28 15:30:33
 */
@Service
public class XxlJobServiceImpl implements XxlJobService {

	private static Logger logger = LoggerFactory.getLogger(XxlJobServiceImpl.class);

	@Resource
	private XxlJobGroupDao xxlJobGroupDao;

	@Resource
	private XxlJobInfoDao xxlJobInfoDao;

	@Resource
	public XxlJobLogDao xxlJobLogDao;

	@Resource
	private XxlJobLogGlueDao xxlJobLogGlueDao;

	@Resource
	private XxlJobLogReportDao xxlJobLogReportDao;

	private boolean isNumeric(String str) {
		try {
			Integer.valueOf(str);
			return true;
		} catch (NumberFormatException e) {
			return false;
		}
	}

	@Override
	public Map<String, Object> pageList(int start, int length, int jobGroup, int triggerStatus, String jobDesc,
			String executorHandler, String author) {
		// page list
		List<XxlJobInfo> list =
				xxlJobInfoDao.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author);
		int list_count =
				xxlJobInfoDao.pageListCount(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author);

		// package result
		Map<String, Object> maps = new HashMap<String, Object>();
		maps.put("recordsTotal", list_count); // 总记录数
		maps.put("recordsFiltered", list_count); // 过滤后的总记录数
		maps.put("data", list); // 分页列表
		return maps;
	}

	private ReturnT<String> checkParam(XxlJobInfo jobInfo) {
		// valid base
		if (jobInfo.getJobDesc() == null || jobInfo.getJobDesc().trim().length() == 0) {
			return new ReturnT<String>(ReturnT.FAIL_CODE,
					(I18nUtil.getString("system_please_input") + I18nUtil.getString("jobinfo_field_jobdesc")));
		}
		if (jobInfo.getAuthor() == null || jobInfo.getAuthor().trim().length() == 0) {
			return new ReturnT<String>(ReturnT.FAIL_CODE,
					(I18nUtil.getString("system_please_input") + I18nUtil.getString("jobinfo_field_author")));
		}

		// valid trigger
		ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(jobInfo.getScheduleType(), null);
		if (scheduleTypeEnum == null) {
			return new ReturnT<String>(ReturnT.FAIL_CODE,
					(I18nUtil.getString("schedule_type") + I18nUtil.getString("system_unvalid")));
		}
		if (scheduleTypeEnum == ScheduleTypeEnum.CRON) {
			if (jobInfo.getScheduleConf() == null || !CronExpression.isValidExpression(jobInfo.getScheduleConf())) {
				return new ReturnT<String>(ReturnT.FAIL_CODE, "Cron" + I18nUtil.getString("system_unvalid"));
			}
		} else if (scheduleTypeEnum == ScheduleTypeEnum.FIX_RATE/* || scheduleTypeEnum == ScheduleTypeEnum.FIX_DELAY */) {
			if (jobInfo.getScheduleConf() == null) {
				return new ReturnT<String>(ReturnT.FAIL_CODE,
						(I18nUtil.getString("schedule_type") + I18nUtil.getString("system_unvalid")));
			}
			try {
				int fixSecond = Integer.valueOf(jobInfo.getScheduleConf());
				if (fixSecond < 1) {
					return new ReturnT<String>(ReturnT.FAIL_CODE,
							(I18nUtil.getString("schedule_type") + I18nUtil.getString("system_unvalid")));
				}
			} catch (Exception e) {
				return new ReturnT<String>(ReturnT.FAIL_CODE,
						(I18nUtil.getString("schedule_type") + I18nUtil.getString("system_unvalid")));
			}
		}

		// valid advanced
		if (ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null) == null) {
			return new ReturnT<String>(ReturnT.FAIL_CODE,
					(I18nUtil.getString("jobinfo_field_executorRouteStrategy") + I18nUtil.getString("system_unvalid")));
		}
		if (MisfireStrategyEnum.match(jobInfo.getMisfireStrategy(), null) == null) {
			return new ReturnT<String>(ReturnT.FAIL_CODE,
					(I18nUtil.getString("misfire_strategy") + I18nUtil.getString("system_unvalid")));
		}
		if (ExecutorBlockStrategyEnum.match(jobInfo.getExecutorBlockStrategy(), null) == null) {
			return new ReturnT<String>(ReturnT.FAIL_CODE,
					(I18nUtil.getString("jobinfo_field_executorBlockStrategy") + I18nUtil.getString("system_unvalid")));
		}

		// valid job
		if (GlueTypeEnum.match(jobInfo.getGlueType()) == null) {
			return new ReturnT<String>(ReturnT.FAIL_CODE,
					(I18nUtil.getString("jobinfo_field_gluetype") + I18nUtil.getString("system_unvalid")));
		}
		if (GlueTypeEnum.BEAN == GlueTypeEnum.match(jobInfo.getGlueType())
				&& (jobInfo.getExecutorHandler() == null || jobInfo.getExecutorHandler().trim().length() == 0)) {
			return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_input") + "JobHandler"));
		}
		// fix "\r" in shell
		if (GlueTypeEnum.GLUE_SHELL == GlueTypeEnum.match(jobInfo.getGlueType()) && jobInfo.getGlueSource() != null) {
			jobInfo.setGlueSource(jobInfo.getGlueSource().replaceAll("\r", ""));
		}

		// ChildJobId valid
		if (jobInfo.getChildJobId() != null && jobInfo.getChildJobId().trim().length() > 0) {
			String[] childJobIds = jobInfo.getChildJobId().split(",");
			for (String childJobIdItem : childJobIds) {
				if (childJobIdItem != null && childJobIdItem.trim().length() > 0 && isNumeric(childJobIdItem)) {
					XxlJobInfo childJobInfo = xxlJobInfoDao.loadById(Integer.parseInt(childJobIdItem));
					if (childJobInfo == null) {
						return new ReturnT<String>(ReturnT.FAIL_CODE,
								MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId") + "({0})"
										+ I18nUtil.getString("system_not_found")), childJobIdItem));
					}
				} else {
					return new ReturnT<String>(ReturnT.FAIL_CODE,
							MessageFormat.format((I18nUtil.getString("jobinfo_field_childJobId") + "({0})"
									+ I18nUtil.getString("system_unvalid")), childJobIdItem));
				}
			}
			// join , avoid "xxx,,"
			String temp = "";
			for (String item : childJobIds) {
				temp += item + ",";
			}
			temp = temp.substring(0, temp.length() - 1);
			jobInfo.setChildJobId(temp);
		}
		// group valid
		if (jobInfo.getJobGroup() > 0) {
			XxlJobGroup group = xxlJobGroupDao.load(jobInfo.getJobGroup());
			if (group == null) {
				return new ReturnT<String>(ReturnT.FAIL_CODE,
						(I18nUtil.getString("system_please_choose") + I18nUtil.getString("jobinfo_field_jobgroup")));
			}
			jobInfo.setAppName(group.getAppname());
		} else {
			if (!StringUtils.hasText(jobInfo.getAppName())) {
				return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_choose")
						+ I18nUtil.getString("jobinfo_field_jobgroup") + " or input the appname"));
			} else {
				List<XxlJobGroup> xxlJobGroups = xxlJobGroupDao.pageList(0, 1, jobInfo.getAppName(), null);
				if (CollectionUtils.isEmpty(xxlJobGroups)) {
					return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("system_please_choose")
							+ I18nUtil.getString("jobinfo_field_jobgroup") + " or input the appname"));
				}
				jobInfo.setJobGroup(xxlJobGroups.get(0).getId());
			}
		}
		return ReturnT.SUCCESS;
	}

	@Override
	public ReturnT<String> add(XxlJobInfo jobInfo) {
		ReturnT<String> checkParam = checkParam(jobInfo);
		if (checkParam.getCode() == ReturnT.FAIL_CODE) {
			return checkParam;
		}
		// executorHandler不能重复
		List<XxlJobInfo> pageList =
				xxlJobInfoDao.pageList(0, 1, jobInfo.getJobGroup(), -1, null, jobInfo.getExecutorHandler(), null);
		if (pageList.size() > 0) {
			return updateBySave(pageList.get(0), jobInfo);
		}
		jobInfo.setAddTime(new Date());
		jobInfo.setUpdateTime(new Date());
		jobInfo.setGlueUpdatetime(new Date());
		xxlJobInfoDao.save(jobInfo);
		if (jobInfo.getId() < 1) {
			return new ReturnT<String>(ReturnT.FAIL_CODE,
					(I18nUtil.getString("jobinfo_field_add") + I18nUtil.getString("system_fail")));
		}
		return new ReturnT<String>(String.valueOf(jobInfo.getId()));
	}

	@Override
	public ReturnT<String> update(XxlJobInfo jobInfo) {
		ReturnT<String> checkParam = checkParam(jobInfo);
		if (checkParam.getCode() == ReturnT.FAIL_CODE) {
			return checkParam;
		}
		XxlJobInfo exists_jobInfo = null;
		if (jobInfo.getId() > 0) {
			exists_jobInfo = xxlJobInfoDao.loadById(jobInfo.getId());
			if (exists_jobInfo == null) {
				return new ReturnT<String>(ReturnT.FAIL_CODE,
						(I18nUtil.getString("jobinfo_field_id") + I18nUtil.getString("system_not_found")));
			}
		} else {
			if (StringUtils.hasText(jobInfo.getExecutorHandler())) {
				List<XxlJobInfo> xxlJobInfos =
						xxlJobInfoDao.selectByAppName(jobInfo.getAppName(), jobInfo.getExecutorHandler());
				if (CollectionUtils.isEmpty(xxlJobInfos)) {
					return new ReturnT<String>(ReturnT.FAIL_CODE,
							("the task for appName:[" + jobInfo.getAppName() + "] and executorHandler:["
									+ jobInfo.getExecutorHandler() + "] " + I18nUtil.getString("system_not_found")));
				}
				exists_jobInfo = xxlJobInfos.get(0);
			} else {
				return new ReturnT<String>(ReturnT.FAIL_CODE,
						("the task for appName:[" + jobInfo.getAppName() + "] and executorHandler:["
								+ jobInfo.getExecutorHandler() + "] " + I18nUtil.getString("system_not_found")));
			}
		}
		return updateBySave(exists_jobInfo, jobInfo);
	}

	private ReturnT<String> updateBySave(XxlJobInfo exists_jobInfo, XxlJobInfo jobInfo) {
		// next trigger time (5s后生效,避开预读周期)
		long nextTriggerTime = exists_jobInfo.getTriggerNextTime();
		boolean scheduleDataNotChanged = jobInfo.getScheduleType().equals(exists_jobInfo.getScheduleType())
				&& jobInfo.getScheduleConf().equals(exists_jobInfo.getScheduleConf());
		if (exists_jobInfo.getTriggerStatus() == 1 && !scheduleDataNotChanged) {
			try {
				Date nextValidTime = JobScheduleHelper.generateNextValidTime(jobInfo,
						new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS));
				if (nextValidTime == null) {
					return new ReturnT<String>(ReturnT.FAIL_CODE,
							(I18nUtil.getString("schedule_type") + I18nUtil.getString("system_unvalid")));
				}
				nextTriggerTime = nextValidTime.getTime();
			} catch (Exception e) {
				logger.error(e.getMessage(), e);
				return new ReturnT<String>(ReturnT.FAIL_CODE,
						(I18nUtil.getString("schedule_type") + I18nUtil.getString("system_unvalid")));
			}
		}

		exists_jobInfo.setJobGroup(jobInfo.getJobGroup());
		exists_jobInfo.setJobDesc(jobInfo.getJobDesc());
		exists_jobInfo.setAuthor(jobInfo.getAuthor());
		exists_jobInfo.setAlarmEmail(jobInfo.getAlarmEmail());
		exists_jobInfo.setScheduleType(jobInfo.getScheduleType());
		exists_jobInfo.setScheduleConf(jobInfo.getScheduleConf());
		exists_jobInfo.setMisfireStrategy(jobInfo.getMisfireStrategy());
		exists_jobInfo.setExecutorRouteStrategy(jobInfo.getExecutorRouteStrategy());
		exists_jobInfo.setExecutorHandler(jobInfo.getExecutorHandler());
		exists_jobInfo.setExecutorParam(jobInfo.getExecutorParam());
		exists_jobInfo.setExecutorBlockStrategy(jobInfo.getExecutorBlockStrategy());
		exists_jobInfo.setExecutorTimeout(jobInfo.getExecutorTimeout());
		exists_jobInfo.setExecutorFailRetryCount(jobInfo.getExecutorFailRetryCount());
		exists_jobInfo.setChildJobId(jobInfo.getChildJobId());
		exists_jobInfo.setTriggerNextTime(nextTriggerTime);
		exists_jobInfo.setUpdateTime(new Date());
		xxlJobInfoDao.update(exists_jobInfo);
		return new ReturnT<String>(exists_jobInfo.getId() + "");
	}

	@Override
	public ReturnT<String> remove(int id) {
		XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
		if (xxlJobInfo == null) {
			return new ReturnT<>(ReturnT.SUCCESS_CODE, "the task id:[" + id + "] not exist");
		}
		xxlJobInfoDao.delete(id);
		xxlJobLogDao.delete(id);
		xxlJobLogGlueDao.deleteByJobId(id);
		return ReturnT.SUCCESS;
	}

	@Override
	public ReturnT<String> removeByName(String appName, String executorHandler) {
		List<XxlJobInfo> xxlJobInfos = xxlJobInfoDao.selectByAppName(appName, executorHandler);
		if (CollectionUtils.isEmpty(xxlJobInfos)) {
			return new ReturnT<>(ReturnT.SUCCESS_CODE, "the task executorHandler:[" + executorHandler + "] not exist");
		}
		return xxlJobInfoDao.delete(xxlJobInfos.get(0).getId()) > 0 ? ReturnT.SUCCESS : ReturnT.FAIL;
	}

	@Override
	public ReturnT<String> start(int id) {
		XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
		if (Objects.isNull(xxlJobInfo)) {
			return new ReturnT<String>(ReturnT.FAIL_CODE, "the task id:[" + id + "] not exist");
		}
		return start(xxlJobInfo);
	}

	@Override
	public ReturnT<String> startByName(String appName, String executorHandler) {
		List<XxlJobInfo> xxlJobInfos = xxlJobInfoDao.selectByAppName(appName, executorHandler);
		if (CollectionUtils.isEmpty(xxlJobInfos)) {
			return new ReturnT<>(ReturnT.FAIL_CODE, "the task executorHandler:[" + executorHandler + "] not exist");
		}
		return start(xxlJobInfos.get(0));
	}

	public ReturnT<String> start(XxlJobInfo xxlJobInfo) {
		// valid
		ScheduleTypeEnum scheduleTypeEnum = ScheduleTypeEnum.match(xxlJobInfo.getScheduleType(), ScheduleTypeEnum.NONE);
		if (ScheduleTypeEnum.NONE == scheduleTypeEnum) {
			return new ReturnT<String>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type_none_limit_start")));
		}
		// next trigger time (5s后生效,避开预读周期)
		long nextTriggerTime = 0;
		try {
			Date nextValidTime = JobScheduleHelper.generateNextValidTime(xxlJobInfo,
					new Date(System.currentTimeMillis() + JobScheduleHelper.PRE_READ_MS));
			if (nextValidTime == null) {
				return new ReturnT<String>(ReturnT.FAIL_CODE,
						(I18nUtil.getString("schedule_type") + I18nUtil.getString("system_unvalid")));
			}
			nextTriggerTime = nextValidTime.getTime();
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			return new ReturnT<>(ReturnT.FAIL_CODE,
					(I18nUtil.getString("schedule_type") + I18nUtil.getString("system_unvalid")));
		}

		xxlJobInfo.setTriggerStatus(1);
		xxlJobInfo.setTriggerLastTime(0);
		xxlJobInfo.setTriggerNextTime(nextTriggerTime);
		xxlJobInfo.setUpdateTime(new Date());
		xxlJobInfoDao.update(xxlJobInfo);
		return ReturnT.SUCCESS;
	}

	@Override
	public ReturnT<String> stop(int id) {
		XxlJobInfo xxlJobInfo = xxlJobInfoDao.loadById(id);
		if (Objects.isNull(xxlJobInfo)) {
			return new ReturnT<>(ReturnT.SUCCESS_CODE, "the task id:[" + id + "] not exist");
		}
		stopUpate(xxlJobInfo);
		return ReturnT.SUCCESS;
	}

	@Override
	public ReturnT<String> stopByName(String appName, String executorHandler) {
		List<XxlJobInfo> xxlJobInfos = xxlJobInfoDao.selectByAppName(appName, executorHandler);
		if (CollectionUtils.isEmpty(xxlJobInfos)) {
			return new ReturnT<>(ReturnT.SUCCESS_CODE, "the task executorHandler:[" + executorHandler + "] not exist");
		}
		stopUpate(xxlJobInfos.get(0));
		return ReturnT.SUCCESS;
	}

	private void stopUpate(XxlJobInfo xxlJobInfo) {
		xxlJobInfo.setTriggerStatus(0);
		xxlJobInfo.setTriggerLastTime(0);
		xxlJobInfo.setTriggerNextTime(0);
		xxlJobInfo.setUpdateTime(new Date());
		xxlJobInfoDao.update(xxlJobInfo);
	}

	@Override
	public ReturnT<String> triggerByName(String appName, String executorHandler, String executorParam,
			String addressList) {
		if (executorParam == null) {
			executorParam = "";
		}
		List<XxlJobInfo> xxlJobInfos = xxlJobInfoDao.selectByAppName(appName, executorHandler);
		if (CollectionUtils.isEmpty(xxlJobInfos)) {
			return new ReturnT<>(ReturnT.FAIL_CODE, "the task executorHandler :[" + executorHandler + "] not exist");
		}
		JobTriggerPoolHelper.trigger(xxlJobInfos.get(0).getId(), TriggerTypeEnum.MANUAL, -1, null, executorParam,
				addressList);
		return ReturnT.SUCCESS;
	}

	@Override
	public Map<String, Object> dashboardInfo() {
		int jobInfoCount = xxlJobInfoDao.findAllCount();
		int jobLogCount = 0;
		int jobLogSuccessCount = 0;
		XxlJobLogReport xxlJobLogReport = xxlJobLogReportDao.queryLogReportTotal();
		if (xxlJobLogReport != null) {
			jobLogCount =
					xxlJobLogReport.getRunningCount() + xxlJobLogReport.getSucCount() + xxlJobLogReport.getFailCount();
			jobLogSuccessCount = xxlJobLogReport.getSucCount();
		}
		// executor count
		Set<String> executorAddressSet = new HashSet<String>();
		List<XxlJobGroup> groupList = xxlJobGroupDao.findAll();
		if (groupList != null && !groupList.isEmpty()) {
			for (XxlJobGroup group : groupList) {
				if (group.getRegistryList() != null && !group.getRegistryList().isEmpty()) {
					executorAddressSet.addAll(group.getRegistryList());
				}
			}
		}

		int executorCount = executorAddressSet.size();
		Map<String, Object> dashboardMap = new HashMap<String, Object>();
		dashboardMap.put("jobInfoCount", jobInfoCount);
		dashboardMap.put("jobLogCount", jobLogCount);
		dashboardMap.put("jobLogSuccessCount", jobLogSuccessCount);
		dashboardMap.put("executorCount", executorCount);
		return dashboardMap;
	}

	@Override
	public ReturnT<Map<String, Object>> chartInfo(Date startDate, Date endDate) {
		// process
		List<String> triggerDayList = new ArrayList<String>();
		List<Integer> triggerDayCountRunningList = new ArrayList<Integer>();
		List<Integer> triggerDayCountSucList = new ArrayList<Integer>();
		List<Integer> triggerDayCountFailList = new ArrayList<Integer>();
		int triggerCountRunningTotal = 0;
		int triggerCountSucTotal = 0;
		int triggerCountFailTotal = 0;
		List<XxlJobLogReport> logReportList = xxlJobLogReportDao.queryLogReport(startDate, endDate);
		if (logReportList != null && logReportList.size() > 0) {
			for (XxlJobLogReport item : logReportList) {
				String day = DateUtil.formatDate(item.getTriggerDay());
				int triggerDayCountRunning = item.getRunningCount();
				int triggerDayCountSuc = item.getSucCount();
				int triggerDayCountFail = item.getFailCount();

				triggerDayList.add(day);
				triggerDayCountRunningList.add(triggerDayCountRunning);
				triggerDayCountSucList.add(triggerDayCountSuc);
				triggerDayCountFailList.add(triggerDayCountFail);
				triggerCountRunningTotal += triggerDayCountRunning;
				triggerCountSucTotal += triggerDayCountSuc;
				triggerCountFailTotal += triggerDayCountFail;
			}
		} else {
			for (int i = -6; i <= 0; i++) {
				triggerDayList.add(DateUtil.formatDate(DateUtil.addDays(new Date(), i)));
				triggerDayCountRunningList.add(0);
				triggerDayCountSucList.add(0);
				triggerDayCountFailList.add(0);
			}
		}

		Map<String, Object> result = new HashMap<String, Object>();
		result.put("triggerDayList", triggerDayList);
		result.put("triggerDayCountRunningList", triggerDayCountRunningList);
		result.put("triggerDayCountSucList", triggerDayCountSucList);
		result.put("triggerDayCountFailList", triggerDayCountFailList);
		result.put("triggerCountRunningTotal", triggerCountRunningTotal);
		result.put("triggerCountSucTotal", triggerCountSucTotal);
		result.put("triggerCountFailTotal", triggerCountFailTotal);
		return new ReturnT<Map<String, Object>>(result);
	}
}